/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2012 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package javax.mail.internet;

import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.Locale;
import javax.mail.*;
import com.sun.mail.util.PropUtil;

/**
 * This class represents an Internet email address using the syntax
 * of <a href="http://www.ietf.org/rfc/rfc822.txt">RFC822</a>.
 * Typical address syntax is of the form "user@host.domain" or
 * "Personal Name &lt;user@host.domain&gt;".
 * 
 * @author Bill Shannon
 * @author John Mani
 */

public class InternetAddress extends Address implements Cloneable {

	protected String address; // email address

	/**
	 * The personal name.
	 */
	protected String personal;

	/**
	 * The RFC 2047 encoded version of the personal name.
	 * <p>
	 * 
	 * This field and the <code>personal</code> field track each other, so if a
	 * subclass sets one of these fields directly, it should set the other to
	 * <code>null</code>, so that it is suitably recomputed.
	 */
	protected String encodedPersonal;

	private static final long serialVersionUID = -7507595530758302903L;

	private static final boolean ignoreBogusGroupName = PropUtil.getBooleanSystemProperty("mail.mime.address.ignorebogusgroupname", true);

	/**
	 * Default constructor.
	 */
	public InternetAddress() {
	}

	/**
	 * Constructor.
	 * <p>
	 * 
	 * Parse the given string and create an InternetAddress. See the
	 * <code>parse</code> method for details of the parsing. The address is
	 * parsed using "strict" parsing. This constructor does <b>not</b> perform
	 * the additional syntax checks that the
	 * <code>InternetAddress(String address, boolean strict)</code> constructor
	 * does when <code>strict</code> is <code>true</code>. This constructor is
	 * equivalent to <code>InternetAddress(address, false)</code>.
	 * 
	 * @param address
	 *            the address in RFC822 format
	 * @exception AddressException
	 *                if the parse failed
	 */
	public InternetAddress(String address) throws AddressException {
		// use our address parsing utility routine to parse the string
		InternetAddress a[] = parse(address, true);
		// if we got back anything other than a single address, it's an error
		if (a.length != 1)
			throw new AddressException("Illegal address", address);

		/*
		 * Now copy the contents of the single address we parsed
		 * into the current object, which will be returned from the
		 * constructor.
		 * XXX - this sure is a round-about way of getting this done.
		 */
		this.address = a[0].address;
		this.personal = a[0].personal;
		this.encodedPersonal = a[0].encodedPersonal;
	}

	/**
	 * Parse the given string and create an InternetAddress.
	 * If <code>strict</code> is false, the detailed syntax of the
	 * address isn't checked.
	 * 
	 * @param address
	 *            the address in RFC822 format
	 * @param strict
	 *            enforce RFC822 syntax
	 * @exception AddressException
	 *                if the parse failed
	 * @since JavaMail 1.3
	 */
	public InternetAddress(String address, boolean strict) throws AddressException {
		this(address);
		if (strict) {
			if (isGroup())
				getGroup(true); // throw away the result
			else
				checkAddress(this.address, true, true);
		}
	}

	/**
	 * Construct an InternetAddress given the address and personal name.
	 * The address is assumed to be a syntactically valid RFC822 address.
	 * 
	 * @param address
	 *            the address in RFC822 format
	 * @param personal
	 *            the personal name
	 */
	public InternetAddress(String address, String personal) throws UnsupportedEncodingException {
		this(address, personal, null);
	}

	/**
	 * Construct an InternetAddress given the address and personal name.
	 * The address is assumed to be a syntactically valid RFC822 address.
	 * 
	 * @param address
	 *            the address in RFC822 format
	 * @param personal
	 *            the personal name
	 * @param charset
	 *            the MIME charset for the name
	 */
	public InternetAddress(String address, String personal, String charset) throws UnsupportedEncodingException {
		this.address = address;
		setPersonal(personal, charset);
	}

	/**
	 * Return a copy of this InternetAddress object.
	 * 
	 * @since JavaMail 1.2
	 */
	public Object clone() {
		InternetAddress a = null;
		try {
			a = (InternetAddress) super.clone();
		} catch (CloneNotSupportedException e) {
		} // Won't happen
		return a;
	}

	/**
	 * Return the type of this address. The type of an InternetAddress
	 * is "rfc822".
	 */
	public String getType() {
		return "rfc822";
	}

	/**
	 * Set the email address.
	 * 
	 * @param address
	 *            email address
	 */
	public void setAddress(String address) {
		this.address = address;
	}

	/**
	 * Set the personal name. If the name contains non US-ASCII
	 * characters, then the name will be encoded using the specified
	 * charset as per RFC 2047. If the name contains only US-ASCII
	 * characters, no encoding is done and the name is used as is.
	 * <p>
	 * 
	 * @param name
	 *            personal name
	 * @param charset
	 *            MIME charset to be used to encode the name as
	 *            per RFC 2047
	 * @see #setPersonal(String)
	 * @exception UnsupportedEncodingException
	 *                if the charset encoding
	 *                fails.
	 */
	public void setPersonal(String name, String charset) throws UnsupportedEncodingException {
		personal = name;
		if (name != null)
			encodedPersonal = MimeUtility.encodeWord(name, charset, null);
		else
			encodedPersonal = null;
	}

	/**
	 * Set the personal name. If the name contains non US-ASCII
	 * characters, then the name will be encoded using the platform's
	 * default charset. If the name contains only US-ASCII characters,
	 * no encoding is done and the name is used as is.
	 * <p>
	 * 
	 * @param name
	 *            personal name
	 * @see #setPersonal(String name, String charset)
	 * @exception UnsupportedEncodingException
	 *                if the charset encoding
	 *                fails.
	 */
	public void setPersonal(String name) throws UnsupportedEncodingException {
		personal = name;
		if (name != null)
			encodedPersonal = MimeUtility.encodeWord(name);
		else
			encodedPersonal = null;
	}

	/**
	 * Get the email address.
	 * 
	 * @return email address
	 */
	public String getAddress() {
		return address;
	}

	/**
	 * Get the personal name. If the name is encoded as per RFC 2047,
	 * it is decoded and converted into Unicode. If the decoding or
	 * conversion fails, the raw data is returned as is.
	 * 
	 * @return personal name
	 */
	public String getPersonal() {
		if (personal != null)
			return personal;

		if (encodedPersonal != null) {
			try {
				personal = MimeUtility.decodeText(encodedPersonal);
				return personal;
			} catch (Exception ex) {
				ex.printStackTrace();
				// 1. ParseException: either its an unencoded string or
				// it can't be parsed
				// 2. UnsupportedEncodingException: can't decode it.
				return encodedPersonal;
			}
		}
		// No personal or encodedPersonal, return null
		return null;
	}

	/**
	 * Convert this address into a RFC 822 / RFC 2047 encoded address.
	 * The resulting string contains only US-ASCII characters, and
	 * hence is mail-safe.
	 * 
	 * @return possibly encoded address string
	 */
	public String toString() {
		if (encodedPersonal == null && personal != null)
			try {
				encodedPersonal = MimeUtility.encodeWord(personal);
			} catch (UnsupportedEncodingException ex) {
			}

		if (address == null || address.length() == 0)
			if (encodedPersonal != null)
				return quotePhrase(encodedPersonal);
			else
				return "";
		if (encodedPersonal != null)
			return quotePhrase(encodedPersonal) + " <" + address + ">";
		else if (isGroup() || isSimple())
			return address;
		else
			return "<" + address + ">";
	}

	/**
	 * Returns a properly formatted address (RFC 822 syntax) of
	 * Unicode characters.
	 * 
	 * @return Unicode address string
	 * @since JavaMail 1.2
	 */
	public String toUnicodeString() {
		String p = getPersonal();
		if (p != null)
			return quotePhrase(p) + " <" + address + ">";
		else if (isGroup() || isSimple())
			return address;
		else
			return "<" + address + ">";
	}

	/*
	 * quotePhrase() quotes the words within a RFC822 phrase.
	 * 
	 * This is tricky, since a phrase is defined as 1 or more
	 * RFC822 words, separated by LWSP. Now, a word that contains
	 * LWSP is supposed to be quoted, and this is exactly what the
	 * MimeUtility.quote() method does. However, when dealing with
	 * a phrase, any LWSP encountered can be construed to be the
	 * separator between words, and not part of the words themselves.
	 * To deal with this funkiness, we have the below variant of
	 * MimeUtility.quote(), which essentially ignores LWSP when
	 * deciding whether to quote a word.
	 * 
	 * It aint pretty, but it gets the job done :)
	 */

	private static final String rfc822phrase = HeaderTokenizer.RFC822.replace(' ', '\0').replace('\t', '\0');

	private static String quotePhrase(String phrase) {
		int len = phrase.length();
		boolean needQuoting = false;

		for (int i = 0; i < len; i++) {
			char c = phrase.charAt(i);
			if (c == '"' || c == '\\') {
				// need to escape them and then quote the whole string
				StringBuffer sb = new StringBuffer(len + 3);
				sb.append('"');
				for (int j = 0; j < len; j++) {
					char cc = phrase.charAt(j);
					if (cc == '"' || cc == '\\')
						// Escape the character
						sb.append('\\');
					sb.append(cc);
				}
				sb.append('"');
				return sb.toString();
			} else if ((c < 040 && c != '\r' && c != '\n' && c != '\t') || c >= 0177 || rfc822phrase.indexOf(c) >= 0)
				// These characters cause the string to be quoted
				needQuoting = true;
		}

		if (needQuoting) {
			StringBuffer sb = new StringBuffer(len + 2);
			sb.append('"').append(phrase).append('"');
			return sb.toString();
		} else
			return phrase;
	}

	private static String unquote(String s) {
		if (s.startsWith("\"") && s.endsWith("\"")) {
			s = s.substring(1, s.length() - 1);
			// check for any escaped characters
			if (s.indexOf('\\') >= 0) {
				StringBuffer sb = new StringBuffer(s.length()); // approx
				for (int i = 0; i < s.length(); i++) {
					char c = s.charAt(i);
					if (c == '\\' && i < s.length() - 1)
						c = s.charAt(++i);
					sb.append(c);
				}
				s = sb.toString();
			}
		}
		return s;
	}

	/**
	 * The equality operator.
	 */
	public boolean equals(Object a) {
		if (!(a instanceof InternetAddress))
			return false;

		String s = ((InternetAddress) a).getAddress();
		if (s == address)
			return true;
		if (address != null && address.equalsIgnoreCase(s))
			return true;

		return false;
	}

	/**
	 * Compute a hash code for the address.
	 */
	public int hashCode() {
		if (address == null)
			return 0;
		else
			return address.toLowerCase(Locale.ENGLISH).hashCode();
	}

	/**
	 * Convert the given array of InternetAddress objects into
	 * a comma separated sequence of address strings. The
	 * resulting string contains only US-ASCII characters, and
	 * hence is mail-safe.
	 * <p>
	 * 
	 * @param addresses
	 *            array of InternetAddress objects
	 * @exception ClassCastException
	 *                , if any address object in the
	 *                given array is not an InternetAddress object. Note
	 *                that this is a RuntimeException.
	 * @return comma separated string of addresses
	 */
	public static String toString(Address[] addresses) {
		return toString(addresses, 0);
	}

	/**
	 * Convert the given array of InternetAddress objects into
	 * a comma separated sequence of address strings. The
	 * resulting string contains only US-ASCII characters, and
	 * hence is mail-safe.
	 * <p>
	 * 
	 * The 'used' parameter specifies the number of character positions already
	 * taken up in the field into which the resulting address sequence string is
	 * to be inserted. It is used to determine the line-break positions in the
	 * resulting address sequence string.
	 * 
	 * @param addresses
	 *            array of InternetAddress objects
	 * @param used
	 *            number of character positions already used, in
	 *            the field into which the address string is to
	 *            be inserted.
	 * @exception ClassCastException
	 *                , if any address object in the
	 *                given array is not an InternetAddress object. Note
	 *                that this is a RuntimeException.
	 * @return comma separated string of addresses
	 */
	public static String toString(Address[] addresses, int used) {
		if (addresses == null || addresses.length == 0)
			return null;

		StringBuffer sb = new StringBuffer();

		for (int i = 0; i < addresses.length; i++) {
			if (i != 0) { // need to append comma
				sb.append(", ");
				used += 2;
			}

			String s = addresses[i].toString();
			int len = lengthOfFirstSegment(s); // length till CRLF
			if (used + len > 76) { // overflows ...
				sb.append("\r\n\t"); // .. start new continuation line
				used = 8; // account for the starting <tab> char
			}
			sb.append(s);
			used = lengthOfLastSegment(s, used);
		}

		return sb.toString();
	}

	/*
	 * Return the length of the first segment within this string.
	 * If no segments exist, the length of the whole line is returned.
	 */
	private static int lengthOfFirstSegment(String s) {
		int pos;
		if ((pos = s.indexOf("\r\n")) != -1)
			return pos;
		else
			return s.length();
	}

	/*
	 * Return the length of the last segment within this string.
	 * If no segments exist, the length of the whole line plus
	 * <code>used</code> is returned.
	 */
	private static int lengthOfLastSegment(String s, int used) {
		int pos;
		if ((pos = s.lastIndexOf("\r\n")) != -1)
			return s.length() - pos - 2;
		else
			return s.length() + used;
	}

	/**
	 * Return an InternetAddress object representing the current user.
	 * The entire email address may be specified in the "mail.from"
	 * property. If not set, the "mail.user" and "mail.host" properties
	 * are tried. If those are not set, the "user.name" property and
	 * <code>InetAddress.getLocalHost</code> method are tried.
	 * Security exceptions that may occur while accessing this information
	 * are ignored. If it is not possible to determine an email address,
	 * null is returned.
	 * 
	 * @param session
	 *            Session object used for property lookup
	 * @return current user's email address
	 */
	public static InternetAddress getLocalAddress(Session session) {
		try {
			return _getLocalAddress(session);
		} catch (SecurityException sex) { // ignore it
		} catch (AddressException ex) { // ignore it
		} catch (UnknownHostException ex) {
		} // ignore it
		return null;
	}

	/**
	 * A package-private version of getLocalAddress that doesn't swallow
	 * the exception. Used by MimeMessage.setFrom() to report the reason
	 * for the failure.
	 */
	// package-private
	static InternetAddress _getLocalAddress(Session session) throws SecurityException, AddressException, UnknownHostException {
		String user = null, host = null, address = null;
		if (session == null) {
			user = System.getProperty("user.name");
			host = getLocalHostName();
		} else {
			address = session.getProperty("mail.from");
			if (address == null) {
				user = session.getProperty("mail.user");
				if (user == null || user.length() == 0)
					user = session.getProperty("user.name");
				if (user == null || user.length() == 0)
					user = System.getProperty("user.name");
				host = session.getProperty("mail.host");
				if (host == null || host.length() == 0)
					host = getLocalHostName();
			}
		}

		if (address == null && user != null && user.length() != 0 && host != null && host.length() != 0)
			address = MimeUtility.quote(user.trim(), specialsNoDot + "\t ") + "@" + host;

		if (address == null)
			return null;

		return new InternetAddress(address);
	}

	/**
	 * Get the local host name from InetAddress and return it in a form
	 * suitable for use in an email address.
	 */
	private static String getLocalHostName() throws UnknownHostException {
		String host = null;
		InetAddress me = InetAddress.getLocalHost();
		if (me != null) {
			host = me.getHostName();
			if (host != null && host.length() > 0 && isInetAddressLiteral(host))
				host = '[' + host + ']';
		}
		return host;
	}

	/**
	 * Is the address an IPv4 or IPv6 address literal, which needs to
	 * be enclosed in "[]" in an email address? IPv4 literals contain
	 * decimal digits and dots, IPv6 literals contain hex digits, dots,
	 * and colons. We're lazy and don't check the exact syntax, just
	 * the allowed characters; strings that have only the allowed
	 * characters in a literal but don't meet the syntax requirements
	 * for a literal definitely can't be a host name and thus will fail
	 * later when used as an address literal.
	 */
	private static boolean isInetAddressLiteral(String addr) {
		boolean sawHex = false, sawColon = false;
		for (int i = 0; i < addr.length(); i++) {
			char c = addr.charAt(i);
			if (c >= '0' && c <= '9')
				; // digits always ok
			else if (c == '.')
				; // dot always ok
			else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
				sawHex = true; // need to see a colon too
			else if (c == ':')
				sawColon = true;
			else
				return false; // anything else, definitely not a literal
		}
		return !sawHex || sawColon;
	}

	/**
	 * Parse the given comma separated sequence of addresses into
	 * InternetAddress objects. Addresses must follow RFC822 syntax.
	 * 
	 * @param addresslist
	 *            comma separated address strings
	 * @return array of InternetAddress objects
	 * @exception AddressException
	 *                if the parse failed
	 */
	public static InternetAddress[] parse(String addresslist) throws AddressException {
		return parse(addresslist, true);
	}

	/**
	 * Parse the given sequence of addresses into InternetAddress
	 * objects. If <code>strict</code> is false, simple email addresses
	 * separated by spaces are also allowed. If <code>strict</code> is
	 * true, many (but not all) of the RFC822 syntax rules are enforced.
	 * In particular, even if <code>strict</code> is true, addresses
	 * composed of simple names (with no "@domain" part) are allowed.
	 * Such "illegal" addresses are not uncommon in real messages.
	 * <p>
	 * 
	 * Non-strict parsing is typically used when parsing a list of mail
	 * addresses entered by a human. Strict parsing is typically used when
	 * parsing address headers in mail messages.
	 * 
	 * @param addresslist
	 *            comma separated address strings
	 * @param strict
	 *            enforce RFC822 syntax
	 * @return array of InternetAddress objects
	 * @exception AddressException
	 *                if the parse failed
	 */
	public static InternetAddress[] parse(String addresslist, boolean strict) throws AddressException {
		return parse(addresslist, strict, false);
	}

	/**
	 * Parse the given sequence of addresses into InternetAddress
	 * objects. If <code>strict</code> is false, the full syntax rules for
	 * individual addresses are not enforced. If <code>strict</code> is
	 * true, many (but not all) of the RFC822 syntax rules are enforced.
	 * <p>
	 * 
	 * To better support the range of "invalid" addresses seen in real messages,
	 * this method enforces fewer syntax rules than the <code>parse</code>
	 * method when the strict flag is false and enforces more rules when the
	 * strict flag is true. If the strict flag is false and the parse is
	 * successful in separating out an email address or addresses, the syntax of
	 * the addresses themselves is not checked.
	 * 
	 * @param addresslist
	 *            comma separated address strings
	 * @param strict
	 *            enforce RFC822 syntax
	 * @return array of InternetAddress objects
	 * @exception AddressException
	 *                if the parse failed
	 * @since JavaMail 1.3
	 */
	public static InternetAddress[] parseHeader(String addresslist, boolean strict) throws AddressException {
		return parse(addresslist, strict, true);
	}

	/*
	 * RFC822 Address parser.
	 * 
	 * XXX - This is complex enough that it ought to be a real parser,
	 * not this ad-hoc mess, and because of that, this is not perfect.
	 * 
	 * XXX - Deal with encoded Headers too.
	 */
	private static InternetAddress[] parse(String s, boolean strict, boolean parseHdr) throws AddressException {
		int start, end, index, nesting;
		int start_personal = -1, end_personal = -1;
		int length = s.length();
		boolean ignoreErrors = parseHdr && !strict;
		boolean in_group = false; // we're processing a group term
		boolean route_addr = false; // address came from route-addr term
		boolean rfc822 = false; // looks like an RFC822 address
		char c;
		List v = new ArrayList();
		InternetAddress ma;

		for (start = end = -1, index = 0; index < length; index++) {
			c = s.charAt(index);

			switch (c) {
			case '(': // We are parsing a Comment. Ignore everything inside.
				// XXX - comment fields should be parsed as whitespace,
				// more than one allowed per address
				rfc822 = true;
				if (start >= 0 && end == -1)
					end = index;
				int pindex = index;
				for (index++, nesting = 1; index < length && nesting > 0; index++) {
					c = s.charAt(index);
					switch (c) {
					case '\\':
						index++; // skip both '\' and the escaped char
						break;
					case '(':
						nesting++;
						break;
					case ')':
						nesting--;
						break;
					default:
						break;
					}
				}
				if (nesting > 0) {
					if (!ignoreErrors)
						throw new AddressException("Missing ')'", s, index);
					// pretend the first paren was a regular character and
					// continue parsing after it
					index = pindex + 1;
					break;
				}
				index--; // point to closing paren
				if (start_personal == -1)
					start_personal = pindex + 1;
				if (end_personal == -1)
					end_personal = index;
				break;

			case ')':
				if (!ignoreErrors)
					throw new AddressException("Missing '('", s, index);
				// pretend the left paren was a regular character and
				// continue parsing
				if (start == -1)
					start = index;
				break;

			case '<':
				rfc822 = true;
				if (route_addr) {
					if (!ignoreErrors)
						throw new AddressException("Extra route-addr", s, index);

					// assume missing comma between addresses
					if (start == -1) {
						route_addr = false;
						rfc822 = false;
						start = end = -1;
						break; // nope, nothing there
					}
					if (!in_group) {
						// got a token, add this to our InternetAddress vector
						if (end == -1) // should never happen
							end = index;
						String addr = s.substring(start, end).trim();

						ma = new InternetAddress();
						ma.setAddress(addr);
						if (start_personal >= 0) {
							ma.encodedPersonal = unquote(s.substring(start_personal, end_personal).trim());
						}
						v.add(ma);

						route_addr = false;
						rfc822 = false;
						start = end = -1;
						start_personal = end_personal = -1;
						// continue processing this new address...
					}
				}

				int rindex = index;
				boolean inquote = false;
				outf:
				for (index++; index < length; index++) {
					c = s.charAt(index);
					switch (c) {
					case '\\': // XXX - is this needed?
						index++; // skip both '\' and the escaped char
						break;
					case '"':
						inquote = !inquote;
						break;
					case '>':
						if (inquote)
							continue;
						break outf; // out of for loop
					default:
						break;
					}
				}

				// did we find a matching quote?
				if (inquote) {
					if (!ignoreErrors)
						throw new AddressException("Missing '\"'", s, index);
					// didn't find matching quote, try again ignoring quotes
					// (e.g., ``<"@foo.com>'')
					outq:
					for (index = rindex + 1; index < length; index++) {
						c = s.charAt(index);
						if (c == '\\') // XXX - is this needed?
							index++; // skip both '\' and the escaped char
						else if (c == '>')
							break;
					}
				}

				// did we find a terminating '>'?
				if (index >= length) {
					if (!ignoreErrors)
						throw new AddressException("Missing '>'", s, index);
					// pretend the "<" was a regular character and
					// continue parsing after it (e.g., ``<@foo.com'')
					index = rindex + 1;
					if (start == -1)
						start = rindex; // back up to include "<"
					break;
				}

				if (!in_group) {
					start_personal = start;
					if (start_personal >= 0)
						end_personal = rindex;
					start = rindex + 1;
				}
				route_addr = true;
				end = index;
				break;

			case '>':
				if (!ignoreErrors)
					throw new AddressException("Missing '<'", s, index);
				// pretend the ">" was a regular character and
				// continue parsing (e.g., ``>@foo.com'')
				if (start == -1)
					start = index;
				break;

			case '"': // parse quoted string
				int qindex = index;
				rfc822 = true;
				if (start == -1)
					start = index;
				outq:
				for (index++; index < length; index++) {
					c = s.charAt(index);
					switch (c) {
					case '\\':
						index++; // skip both '\' and the escaped char
						break;
					case '"':
						break outq; // out of for loop
					default:
						break;
					}
				}
				if (index >= length) {
					if (!ignoreErrors)
						throw new AddressException("Missing '\"'", s, index);
					// pretend the quote was a regular character and
					// continue parsing after it (e.g., ``"@foo.com'')
					index = qindex + 1;
				}
				break;

			case '[': // a domain-literal, probably
				rfc822 = true;
				int lindex = index;
				outb:
				for (index++; index < length; index++) {
					c = s.charAt(index);
					switch (c) {
					case '\\':
						index++; // skip both '\' and the escaped char
						break;
					case ']':
						break outb; // out of for loop
					default:
						break;
					}
				}
				if (index >= length) {
					if (!ignoreErrors)
						throw new AddressException("Missing ']'", s, index);
					// pretend the "[" was a regular character and
					// continue parsing after it (e.g., ``[@foo.com'')
					index = lindex + 1;
				}
				break;

			case ';':
				if (start == -1) {
					route_addr = false;
					rfc822 = false;
					start = end = -1;
					break; // nope, nothing there
				}
				if (in_group) {
					in_group = false;
					/*
					 * If parsing headers, but not strictly, peek ahead.
					 * If next char is "@", treat the group name
					 * like the local part of the address, e.g.,
					 * "Undisclosed-Recipient:;@java.sun.com".
					 */
					if (parseHdr && !strict && index + 1 < length && s.charAt(index + 1) == '@')
						break;
					ma = new InternetAddress();
					end = index + 1;
					ma.setAddress(s.substring(start, end).trim());
					v.add(ma);

					route_addr = false;
					rfc822 = false;
					start = end = -1;
					start_personal = end_personal = -1;
					break;
				}
				if (!ignoreErrors)
					throw new AddressException("Illegal semicolon, not in group", s, index);

				// otherwise, parsing a header; treat semicolon like comma
				// fall through to comma case...

			case ',': // end of an address, probably
				if (start == -1) {
					route_addr = false;
					rfc822 = false;
					start = end = -1;
					break; // nope, nothing there
				}
				if (in_group) {
					route_addr = false;
					break;
				}
				// got a token, add this to our InternetAddress vector
				if (end == -1)
					end = index;

				String addr = s.substring(start, end).trim();
				String pers = null;
				if (rfc822 && start_personal >= 0) {
					pers = unquote(s.substring(start_personal, end_personal).trim());
					if (pers.trim().length() == 0)
						pers = null;
				}

				/*
				 * If the personal name field has an "@" and the address
				 * field does not, assume they were reversed, e.g.,
				 * ``"joe doe" (john.doe@example.com)''.
				 */
				if (parseHdr && !strict && pers != null && pers.indexOf('@') >= 0 && addr.indexOf('@') < 0 && addr.indexOf('!') < 0) {
					String tmp = addr;
					addr = pers;
					pers = tmp;
				}
				if (rfc822 || strict || parseHdr) {
					if (!ignoreErrors)
						checkAddress(addr, route_addr, false);
					ma = new InternetAddress();
					ma.setAddress(addr);
					if (pers != null)
						ma.encodedPersonal = pers;
					v.add(ma);
				} else {
					// maybe we passed over more than one space-separated addr
					StringTokenizer st = new StringTokenizer(addr);
					while (st.hasMoreTokens()) {
						String a = st.nextToken();
						checkAddress(a, false, false);
						ma = new InternetAddress();
						ma.setAddress(a);
						v.add(ma);
					}
				}

				route_addr = false;
				rfc822 = false;
				start = end = -1;
				start_personal = end_personal = -1;
				break;

			case ':':
				rfc822 = true;
				if (in_group)
					if (!ignoreErrors)
						throw new AddressException("Nested group", s, index);
				if (start == -1)
					start = index;
				if (parseHdr && !strict) {
					/*
					 * If next char is a special character that can't occur at
					 * the start of a valid address, treat the group name
					 * as the entire address, e.g., "Date:, Tue", "Re:@foo".
					 */
					if (index + 1 < length) {
						String addressSpecials = ")>[]:@\\,.";
						char nc = s.charAt(index + 1);
						if (addressSpecials.indexOf(nc) >= 0) {
							if (nc != '@')
								break; // don't change in_group
							/*
							 * Handle a common error:
							 * ``Undisclosed-Recipient:@example.com;''
							 * 
							 * Scan ahead. If we find a semicolon before
							 * one of these other special characters,
							 * consider it to be a group after all.
							 */
							for (int i = index + 2; i < length; i++) {
								nc = s.charAt(i);
								if (nc == ';')
									break;
								if (addressSpecials.indexOf(nc) >= 0)
									break;
							}
							if (nc == ';')
								break; // don't change in_group
						}
					}

					// ignore bogus "mailto:" prefix in front of an address,
					// or bogus mail header name included in the address field
					String gname = s.substring(start, index);
					if (ignoreBogusGroupName && (gname.equalsIgnoreCase("mailto") || gname.equalsIgnoreCase("From") || gname.equalsIgnoreCase("To") || gname.equalsIgnoreCase("Cc") || gname.equalsIgnoreCase("Subject") || gname.equalsIgnoreCase("Re")))
						start = -1; // we're not really in a group
					else
						in_group = true;
				} else
					in_group = true;
				break;

			// Ignore whitespace
			case ' ':
			case '\t':
			case '\r':
			case '\n':
				break;

			default:
				if (start == -1)
					start = index;
				break;
			}
		}

		if (start >= 0) {
			/*
			 * The last token, add this to our InternetAddress vector.
			 * Note that this block of code should be identical to the
			 * block above for "case ','".
			 */
			if (end == -1)
				end = length;

			String addr = s.substring(start, end).trim();
			String pers = null;
			if (rfc822 && start_personal >= 0) {
				pers = unquote(s.substring(start_personal, end_personal).trim());
				if (pers.trim().length() == 0)
					pers = null;
			}

			/*
			 * If the personal name field has an "@" and the address
			 * field does not, assume they were reversed, e.g.,
			 * ``"joe doe" (john.doe@example.com)''.
			 */
			if (parseHdr && !strict && pers != null && pers.indexOf('@') >= 0 && addr.indexOf('@') < 0 && addr.indexOf('!') < 0) {
				String tmp = addr;
				addr = pers;
				pers = tmp;
			}
			if (rfc822 || strict || parseHdr) {
				if (!ignoreErrors)
					checkAddress(addr, route_addr, false);
				ma = new InternetAddress();
				ma.setAddress(addr);
				if (pers != null)
					ma.encodedPersonal = pers;
				v.add(ma);
			} else {
				// maybe we passed over more than one space-separated addr
				StringTokenizer st = new StringTokenizer(addr);
				while (st.hasMoreTokens()) {
					String a = st.nextToken();
					checkAddress(a, false, false);
					ma = new InternetAddress();
					ma.setAddress(a);
					v.add(ma);
				}
			}
		}

		InternetAddress[] a = new InternetAddress[v.size()];
		v.toArray(a);
		return a;
	}

	/**
	 * Validate that this address conforms to the syntax rules of
	 * RFC 822. The current implementation checks many, but not
	 * all, syntax rules. Note that even though the syntax of
	 * the address may be correct, there's no guarantee that a
	 * mailbox of that name exists.
	 * 
	 * @exception AddressException
	 *                if the address isn't valid.
	 * @since JavaMail 1.3
	 */
	public void validate() throws AddressException {
		if (isGroup())
			getGroup(true); // throw away the result
		else
			checkAddress(getAddress(), true, true);
	}

	private static final String specialsNoDotNoAt = "()<>,;:\\\"[]";
	private static final String specialsNoDot = specialsNoDotNoAt + "@";

	/**
	 * Check that the address is a valid "mailbox" per RFC822.
	 * (We also allow simple names.)
	 * 
	 * XXX - much more to check
	 * XXX - doesn't handle domain-literals properly (but no one uses them)
	 */
	private static void checkAddress(String addr, boolean routeAddr, boolean validate) throws AddressException {
		int i, start = 0;

		int len = addr.length();
		if (len == 0)
			throw new AddressException("Empty address", addr);

		/*
		 * routeAddr indicates that the address is allowed
		 * to have an RFC 822 "route".
		 */
		if (routeAddr && addr.charAt(0) == '@') {
			/*
			 * Check for a legal "route-addr":
			 * [@domain[,@domain ...]:]local@domain
			 */
			for (start = 0; (i = indexOfAny(addr, ",:", start)) >= 0; start = i + 1) {
				if (addr.charAt(start) != '@')
					throw new AddressException("Illegal route-addr", addr);
				if (addr.charAt(i) == ':') {
					// end of route-addr
					start = i + 1;
					break;
				}
			}
		}

		/*
		 * The rest should be "local@domain", but we allow simply "local"
		 * unless called from validate.
		 * 
		 * local-part must follow RFC 822 - no specials except '.'
		 * unless quoted.
		 */

		char c = (char) -1;
		char lastc = (char) -1;
		boolean inquote = false;
		for (i = start; i < len; i++) {
			lastc = c;
			c = addr.charAt(i);
			// a quoted-pair is only supposed to occur inside a quoted string,
			// but some people use it outside so we're more lenient
			if (c == '\\' || lastc == '\\')
				continue;
			if (c == '"') {
				if (inquote) {
					// peek ahead, next char must be "@"
					if (validate && i + 1 < len && addr.charAt(i + 1) != '@')
						throw new AddressException("Quote not at end of local address", addr);
					inquote = false;
				} else {
					if (validate && i != 0)
						throw new AddressException("Quote not at start of local address", addr);
					inquote = true;
				}
				continue;
			}
			if (inquote)
				continue;
			if (c == '@') {
				if (i == 0)
					throw new AddressException("Missing local name", addr);
				break; // done with local part
			}
			if (c <= 040 || c >= 0177)
				throw new AddressException("Local address contains control or whitespace", addr);
			if (specialsNoDot.indexOf(c) >= 0)
				throw new AddressException("Local address contains illegal character", addr);
		}
		if (inquote)
			throw new AddressException("Unterminated quote", addr);

		/*
		 * Done with local part, now check domain.
		 * 
		 * Note that the MimeMessage class doesn't remember addresses
		 * as separate objects; it writes them out as headers and then
		 * parses the headers when the addresses are requested.
		 * In order to support the case where a "simple" address is used,
		 * but the address also has a personal name and thus looks like
		 * it should be a valid RFC822 address when parsed, we only check
		 * this if we're explicitly called from the validate method.
		 */

		if (c != '@') {
			if (validate)
				throw new AddressException("Missing final '@domain'", addr);
			return;
		}

		// check for illegal chars in the domain, but ignore domain literals

		start = i + 1;
		if (start >= len)
			throw new AddressException("Missing domain", addr);

		if (addr.charAt(start) == '.')
			throw new AddressException("Domain starts with dot", addr);
		for (i = start; i < len; i++) {
			c = addr.charAt(i);
			if (c == '[')
				return; // domain literal, don't validate

			// ignore wrong character
			// if (c <= 040 || c >= 0177)
			// throw new AddressException(
			// "Domain contains control or whitespace", addr);

			// RFC 2822 rule
			// if (specialsNoDot.indexOf(c) >= 0)
			/*
			 * RFC 1034 rule is more strict
			 * the full rule is:
			 * 
			 * <domain> ::= <subdomain> | " "
			 * <subdomain> ::= <label> | <subdomain> "." <label>
			 * <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
			 * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
			 * <let-dig-hyp> ::= <let-dig> | "-"
			 * <let-dig> ::= <letter> | <digit>
			 */
			if (!(Character.isLetterOrDigit(c) || c == '-' || c == '.'))
				throw new AddressException("Domain contains illegal character", addr);
			if (c == '.' && lastc == '.')
				throw new AddressException("Domain contains dot-dot", addr);
			lastc = c;
		}
		if (lastc == '.')
			throw new AddressException("Domain ends with dot", addr);
	}

	/**
	 * Is this a "simple" address? Simple addresses don't contain quotes
	 * or any RFC822 special characters other than '@' and '.'.
	 */
	private boolean isSimple() {
		return address == null || indexOfAny(address, specialsNoDotNoAt) < 0;
	}

	/**
	 * Indicates whether this address is an RFC 822 group address.
	 * Note that a group address is different than the mailing
	 * list addresses supported by most mail servers. Group addresses
	 * are rarely used; see RFC 822 for details.
	 * 
	 * @return true if this address represents a group
	 * @since JavaMail 1.3
	 */
	public boolean isGroup() {
		// quick and dirty check
		return address != null && address.endsWith(";") && address.indexOf(':') > 0;
	}

	/**
	 * Return the members of a group address. A group may have zero,
	 * one, or more members. If this address is not a group, null
	 * is returned. The <code>strict</code> parameter controls whether
	 * the group list is parsed using strict RFC 822 rules or not.
	 * The parsing is done using the <code>parseHeader</code> method.
	 * 
	 * @return array of InternetAddress objects, or null
	 * @exception AddressException
	 *                if the group list can't be parsed
	 * @since JavaMail 1.3
	 */
	public InternetAddress[] getGroup(boolean strict) throws AddressException {
		String addr = getAddress();
		// groups are of the form "name:addr,addr,...;"
		if (!addr.endsWith(";"))
			return null;
		int ix = addr.indexOf(':');
		if (ix < 0)
			return null;
		// extract the list
		String list = addr.substring(ix + 1, addr.length() - 1);
		// parse it and return the individual addresses
		return InternetAddress.parseHeader(list, strict);
	}

	/**
	 * Return the first index of any of the characters in "any" in "s",
	 * or -1 if none are found.
	 * 
	 * This should be a method on String.
	 */
	private static int indexOfAny(String s, String any) {
		return indexOfAny(s, any, 0);
	}

	private static int indexOfAny(String s, String any, int start) {
		try {
			int len = s.length();
			for (int i = start; i < len; i++) {
				if (any.indexOf(s.charAt(i)) >= 0)
					return i;
			}
			return -1;
		} catch (StringIndexOutOfBoundsException e) {
			return -1;
		}
	}

	/*
	 * public static void main(String argv[]) throws Exception {
	 * for (int i = 0; i < argv.length; i++) {
	 * InternetAddress[] a = InternetAddress.parse(argv[i]);
	 * for (int j = 0; j < a.length; j++) {
	 * System.out.println("arg " + i + " address " + j + ": " + a[j]);
	 * System.out.println("\tAddress: " + a[j].getAddress() +
	 * "\tPersonal: " + a[j].getPersonal());
	 * }
	 * if (a.length > 1) {
	 * System.out.println("address 0 hash code: " + a[0].hashCode());
	 * System.out.println("address 1 hash code: " + a[1].hashCode());
	 * if (a[0].hashCode() == a[1].hashCode())
	 * System.out.println("success, hashcodes equal");
	 * else
	 * System.out.println("fail, hashcodes not equal");
	 * if (a[0].equals(a[1]))
	 * System.out.println("success, addresses equal");
	 * else
	 * System.out.println("fail, addresses not equal");
	 * if (a[1].equals(a[0]))
	 * System.out.println("success, addresses equal");
	 * else
	 * System.out.println("fail, addresses not equal");
	 * }
	 * }
	 * }
	 */
}
